/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
 * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
 * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.apache.tuweni.bytes;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.nio.ByteOrder.BIG_ENDIAN;

import io.netty.buffer.ByteBuf;
import io.vertx.core.buffer.Buffer;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigInteger;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ReadOnlyBufferException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Random;

/**
 * A value made of bytes.
 *
 * <p>This interface makes no thread-safety guarantee, and a {@link Bytes} value is generally not
 * thread safe. However, specific implementations may be thread-safe. For instance, the value
 * returned by {@link #copy} is guaranteed to be thread-safe as it is immutable.
 */
public interface Bytes extends Comparable<Bytes> {

    /** The empty value (with 0 bytes). */
    Bytes EMPTY = wrap(new byte[0]);

    /**
     * Wrap the provided byte array as a {@link Bytes} value.
     *
     * <p>Note that value is not copied and thus any future update to {@code value} will be
     * reflected in the returned value.
     *
     * @param value The value to wrap.
     * @return A {@link Bytes} value wrapping {@code value}.
     */
    static Bytes wrap(byte[] value) {
        return wrap(value, 0, value.length);
    }

    /**
     * Wrap a slice of a byte array as a {@link Bytes} value.
     *
     * <p>Note that value is not copied and thus any future update to {@code value} within the slice
     * will be reflected in the returned value.
     *
     * @param value The value to wrap.
     * @param offset The index (inclusive) in {@code value} of the first byte exposed by the
     *     returned value. In other words, you will have {@code wrap(value, o, l).get(0) ==
     *     value[o]}.
     * @param length The length of the resulting value.
     * @return A {@link Bytes} value that expose the bytes of {@code value} from {@code offset}
     *     (inclusive) to {@code offset + length} (exclusive).
     * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >=
     *     value.length)}.
     * @throws IllegalArgumentException if {@code length < 0 || offset + length > value.length}.
     */
    static Bytes wrap(byte[] value, int offset, int length) {
        checkNotNull(value);
        if (length == 32) {
            return new ArrayWrappingBytes32(value, offset);
        }
        return new ArrayWrappingBytes(value, offset, length);
    }

    /**
     * Wrap the provided byte array as a {@link Bytes} value, encrypted in memory.
     *
     * @param value The value to secure.
     * @return A {@link Bytes} value securing {@code value}.
     */
    static Bytes secure(byte[] value) {
        return secure(value, 0, value.length);
    }

    /**
     * Wrap a slice of a byte array as a {@link Bytes} value, encrypted in memory.
     *
     * @param value The value to secure.
     * @param offset The index (inclusive) in {@code value} of the first byte exposed by the
     *     returned value. In other words, you will have {@code wrap(value, o, l).get(0) ==
     *     value[o]}.
     * @param length The length of the resulting value.
     * @return A {@link Bytes} value that holds securely the bytes of {@code value} from {@code
     *     offset} (inclusive) to {@code offset + length} (exclusive).
     * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >=
     *     value.length)}.
     * @throws IllegalArgumentException if {@code length < 0 || offset + length > value.length}.
     */
    static Bytes secure(byte[] value, int offset, int length) {
        checkNotNull(value);
        return new GuardedByteArrayBytes(value, offset, length);
    }

    /**
     * Wrap a list of other values into a concatenated view.
     *
     * <p>Note that the values are not copied and thus any future update to the values will be
     * reflected in the returned value. If copying the inputs is desired, use {@link
     * #concatenate(Bytes...)}.
     *
     * @param values The values to wrap.
     * @return A value representing a view over the concatenation of all {@code values}.
     * @throws IllegalArgumentException if the result overflows an int.
     */
    static Bytes wrap(Bytes... values) {
        return ConcatenatedBytes.wrap(values);
    }

    /**
     * Create a value containing the concatenation of the values provided.
     *
     * @param values The values to copy and concatenate.
     * @return A value containing the result of concatenating the value from {@code values} in their
     *     provided order.
     * @throws IllegalArgumentException if the result overflows an int.
     */
    static Bytes concatenate(List<Bytes> values) {
        if (values.size() == 0) {
            return EMPTY;
        }

        int size;
        try {
            size = values.stream().mapToInt(Bytes::size).reduce(0, Math::addExact);
        } catch (ArithmeticException e) {
            throw new IllegalArgumentException(
                    "Combined length of values is too long (> Integer.MAX_VALUE)");
        }

        MutableBytes result = MutableBytes.create(size);
        int offset = 0;
        for (Bytes value : values) {
            value.copyTo(result, offset);
            offset += value.size();
        }
        return result;
    }

    /**
     * Create a value containing the concatenation of the values provided.
     *
     * @param values The values to copy and concatenate.
     * @return A value containing the result of concatenating the value from {@code values} in their
     *     provided order.
     * @throws IllegalArgumentException if the result overflows an int.
     */
    static Bytes concatenate(Bytes... values) {
        if (values.length == 0) {
            return EMPTY;
        }

        int size;
        try {
            size = Arrays.stream(values).mapToInt(Bytes::size).reduce(0, Math::addExact);
        } catch (ArithmeticException e) {
            throw new IllegalArgumentException(
                    "Combined length of values is too long (> Integer.MAX_VALUE)");
        }

        MutableBytes result = MutableBytes.create(size);
        int offset = 0;
        for (Bytes value : values) {
            value.copyTo(result, offset);
            offset += value.size();
        }
        return result;
    }

    /**
     * Wrap a full Vert.x {@link Buffer} as a {@link Bytes} value.
     *
     * <p>Note that any change to the content of the buffer may be reflected in the returned value.
     *
     * @param buffer The buffer to wrap.
     * @return A {@link Bytes} value.
     */
    static Bytes wrapBuffer(Buffer buffer) {
        checkNotNull(buffer);
        if (buffer.length() == 0) {
            return EMPTY;
        }
        return new BufferWrappingBytes(buffer);
    }

    /**
     * Wrap a slice of a Vert.x {@link Buffer} as a {@link Bytes} value.
     *
     * <p>Note that any change to the content of the buffer may be reflected in the returned value.
     *
     * @param buffer The buffer to wrap.
     * @param offset The offset in {@code buffer} from which to expose the bytes in the returned
     *     value. That is, {@code wrapBuffer(buffer, i, 1).get(0) == buffer.getByte(i)}.
     * @param size The size of the returned value.
     * @return A {@link Bytes} value.
     * @throws IndexOutOfBoundsException if {@code offset < 0 || (buffer.length() > 0 && offset >=
     *     buffer.length())}.
     * @throws IllegalArgumentException if {@code length < 0 || offset + length > buffer.length()}.
     */
    static Bytes wrapBuffer(Buffer buffer, int offset, int size) {
        checkNotNull(buffer);
        if (size == 0) {
            return EMPTY;
        }
        return new BufferWrappingBytes(buffer, offset, size);
    }

    /**
     * Wrap a full Netty {@link ByteBuf} as a {@link Bytes} value.
     *
     * <p>Note that any change to the content of the byteBuf may be reflected in the returned value.
     *
     * @param byteBuf The {@link ByteBuf} to wrap.
     * @return A {@link Bytes} value.
     */
    static Bytes wrapByteBuf(ByteBuf byteBuf) {
        checkNotNull(byteBuf);
        if (byteBuf.capacity() == 0) {
            return EMPTY;
        }
        return new ByteBufWrappingBytes(byteBuf);
    }

    /**
     * Wrap a slice of a Netty {@link ByteBuf} as a {@link Bytes} value.
     *
     * <p>Note that any change to the content of the buffer may be reflected in the returned value.
     *
     * @param byteBuf The {@link ByteBuf} to wrap.
     * @param offset The offset in {@code byteBuf} from which to expose the bytes in the returned
     *     value. That is, {@code wrapByteBuf(byteBuf, i, 1).get(0) == byteBuf.getByte(i)}.
     * @param size The size of the returned value.
     * @return A {@link Bytes} value.
     * @throws IndexOutOfBoundsException if {@code offset < 0 || (byteBuf.capacity() > 0 && offset
     *     >= byteBuf.capacity())}.
     * @throws IllegalArgumentException if {@code length < 0 || offset + length >
     *     byteBuf.capacity()}.
     */
    static Bytes wrapByteBuf(ByteBuf byteBuf, int offset, int size) {
        checkNotNull(byteBuf);
        if (size == 0) {
            return EMPTY;
        }
        return new ByteBufWrappingBytes(byteBuf, offset, size);
    }

    /**
     * Wrap a full Java NIO {@link ByteBuffer} as a {@link Bytes} value.
     *
     * <p>Note that any change to the content of the byteBuf may be reflected in the returned value.
     *
     * @param byteBuffer The {@link ByteBuffer} to wrap.
     * @return A {@link Bytes} value.
     */
    static Bytes wrapByteBuffer(ByteBuffer byteBuffer) {
        checkNotNull(byteBuffer);
        if (byteBuffer.limit() == 0) {
            return EMPTY;
        }
        return new ByteBufferWrappingBytes(byteBuffer);
    }

    /**
     * Wrap a slice of a Java NIO {@link ByteBuf} as a {@link Bytes} value.
     *
     * <p>Note that any change to the content of the buffer may be reflected in the returned value.
     *
     * @param byteBuffer The {@link ByteBuffer} to wrap.
     * @param offset The offset in {@code byteBuffer} from which to expose the bytes in the returned
     *     value. That is, {@code wrapByteBuffer(byteBuffer, i, 1).get(0) == byteBuffer.getByte(i)}.
     * @param size The size of the returned value.
     * @return A {@link Bytes} value.
     * @throws IndexOutOfBoundsException if {@code offset < 0 || (byteBuffer.limit() > 0 && offset
     *     >= byteBuf.limit())}.
     * @throws IllegalArgumentException if {@code length < 0 || offset + length >
     *     byteBuffer.limit()}.
     */
    static Bytes wrapByteBuffer(ByteBuffer byteBuffer, int offset, int size) {
        checkNotNull(byteBuffer);
        if (size == 0) {
            return EMPTY;
        }
        return new ByteBufferWrappingBytes(byteBuffer, offset, size);
    }

    /**
     * Create a value that contains the specified bytes in their specified order.
     *
     * @param bytes The bytes that must compose the returned value.
     * @return A value containing the specified bytes.
     */
    static Bytes of(byte... bytes) {
        return wrap(bytes);
    }

    /**
     * Create a value that contains the specified bytes in their specified order.
     *
     * @param bytes The bytes.
     * @return A value containing bytes are the one from {@code bytes}.
     * @throws IllegalArgumentException if any of the specified would be truncated when storing as a
     *     byte.
     */
    static Bytes of(int... bytes) {
        byte[] result = new byte[bytes.length];
        for (int i = 0; i < bytes.length; i++) {
            int b = bytes[i];
            checkArgument(b == (((byte) b) & 0xff), "%sth value %s does not fit a byte", i + 1, b);
            result[i] = (byte) b;
        }
        return Bytes.wrap(result);
    }

    /**
     * Return a 2-byte value corresponding to the provided value interpreted as an unsigned short.
     *
     * @param value The value, which must be no larger than an unsigned short.
     * @return A 2 bytes value corresponding to {@code value}.
     * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an
     *     unsigned 2-byte short (that is, if {@code value >= (1 << 16)}).
     */
    static Bytes ofUnsignedShort(int value) {
        return ofUnsignedShort(value, BIG_ENDIAN);
    }

    /**
     * Return a 2-byte value corresponding to the provided value interpreted as an unsigned short.
     *
     * @param value The value, which must be no larger than an unsigned short.
     * @param order The byte-order for the integer encoding.
     * @return A 2 bytes value corresponding to {@code value}.
     * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an
     *     unsigned 2-byte short (that is, if {@code value >= (1 << 16)}).
     */
    static Bytes ofUnsignedShort(int value, ByteOrder order) {
        checkArgument(
                value >= 0 && value <= BytesValues.MAX_UNSIGNED_SHORT,
                "Value %s cannot be represented as an unsigned short (it is negative or too big)",
                value);
        byte[] res = new byte[2];
        if (order == BIG_ENDIAN) {
            res[0] = (byte) ((value >> 8) & 0xFF);
            res[1] = (byte) (value & 0xFF);
        } else {
            res[0] = (byte) (value & 0xFF);
            res[1] = (byte) ((value >> 8) & 0xFF);
        }
        return Bytes.wrap(res);
    }

    /**
     * Return a 4-byte value corresponding to the provided value interpreted as an unsigned int.
     *
     * @param value The value, which must be no larger than an unsigned int.
     * @return A 4 bytes value corresponding to {@code value}.
     * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an
     *     unsigned 4-byte int (that is, if {@code value >= (1L << 32)}).
     */
    static Bytes ofUnsignedInt(long value) {
        return ofUnsignedInt(value, BIG_ENDIAN);
    }

    /**
     * Return a 4-byte value corresponding to the provided value interpreted as an unsigned int.
     *
     * @param value The value, which must be no larger than an unsigned int.
     * @param order The byte-order for the integer encoding.
     * @return A 4 bytes value corresponding to the encoded {@code value}.
     * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an
     *     unsigned 4-byte int (that is, if {@code value >= (1L << 32)}).
     */
    static Bytes ofUnsignedInt(long value, ByteOrder order) {
        checkArgument(
                value >= 0 && value <= BytesValues.MAX_UNSIGNED_INT,
                "Value %s cannot be represented as an unsigned int (it is negative or too big)",
                value);
        byte[] res = new byte[4];
        if (order == BIG_ENDIAN) {
            res[0] = (byte) ((value >> 24) & 0xFF);
            res[1] = (byte) ((value >> 16) & 0xFF);
            res[2] = (byte) ((value >> 8) & 0xFF);
            res[3] = (byte) ((value) & 0xFF);
        } else {
            res[0] = (byte) ((value) & 0xFF);
            res[1] = (byte) ((value >> 8) & 0xFF);
            res[2] = (byte) ((value >> 16) & 0xFF);
            res[3] = (byte) ((value >> 24) & 0xFF);
        }
        return Bytes.wrap(res);
    }

    /**
     * Return an 8-byte value corresponding to the provided value interpreted as an unsigned long.
     *
     * @param value The value, which will be interpreted as an unsigned long.
     * @return A 8 bytes value corresponding to {@code value}.
     * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an
     *     unsigned 8-byte int (that is, if {@code value >= (1L << 64)}).
     */
    static Bytes ofUnsignedLong(long value) {
        return ofUnsignedLong(value, BIG_ENDIAN);
    }

    /**
     * Return an 8-byte value corresponding to the provided value interpreted as an unsigned long.
     *
     * @param value The value, which will be interpreted as an unsigned long.
     * @param order The byte-order for the integer encoding.
     * @return A 8 bytes value corresponding to {@code value}.
     * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an
     *     unsigned 8-byte int (that is, if {@code value >= (1L << 64)}).
     */
    static Bytes ofUnsignedLong(long value, ByteOrder order) {
        byte[] res = new byte[8];
        if (order == BIG_ENDIAN) {
            res[0] = (byte) ((value >> 56) & 0xFF);
            res[1] = (byte) ((value >> 48) & 0xFF);
            res[2] = (byte) ((value >> 40) & 0xFF);
            res[3] = (byte) ((value >> 32) & 0xFF);
            res[4] = (byte) ((value >> 24) & 0xFF);
            res[5] = (byte) ((value >> 16) & 0xFF);
            res[6] = (byte) ((value >> 8) & 0xFF);
            res[7] = (byte) (value & 0xFF);
        } else {
            res[0] = (byte) ((value) & 0xFF);
            res[1] = (byte) ((value >> 8) & 0xFF);
            res[2] = (byte) ((value >> 16) & 0xFF);
            res[3] = (byte) ((value >> 24) & 0xFF);
            res[4] = (byte) ((value >> 32) & 0xFF);
            res[5] = (byte) ((value >> 40) & 0xFF);
            res[6] = (byte) ((value >> 48) & 0xFF);
            res[7] = (byte) ((value >> 56) & 0xFF);
        }
        return Bytes.wrap(res);
    }

    /**
     * Return the smallest bytes value whose bytes correspond to the provided long. That is, the
     * returned value may be of size less than 8 if the provided long has leading zero bytes.
     *
     * @param value The long from which to create the bytes value.
     * @return The minimal bytes representation corresponding to {@code l}.
     */
    static Bytes minimalBytes(long value) {
        if (value == 0) {
            return Bytes.EMPTY;
        }

        int zeros = Long.numberOfLeadingZeros(value);
        int resultBytes = 8 - (zeros / 8);

        byte[] result = new byte[resultBytes];
        int shift = 0;
        for (int i = 0; i < resultBytes; i++) {
            result[resultBytes - i - 1] = (byte) ((value >> shift) & 0xFF);
            shift += 8;
        }
        return Bytes.wrap(result);
    }

    /**
     * Parse a hexadecimal string into a {@link Bytes} value.
     *
     * <p>This method is lenient in that {@code str} may of an odd length, in which case it will
     * behave exactly as if it had an additional 0 in front.
     *
     * @param str The hexadecimal string to parse, which may or may not start with "0x".
     * @return The value corresponding to {@code str}.
     * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal
     *     representation.
     */
    static Bytes fromHexStringLenient(CharSequence str) {
        checkNotNull(str);
        return BytesValues.fromHexString(str, -1, true);
    }

    /**
     * Parse a hexadecimal string into a {@link Bytes} value of the provided size.
     *
     * <p>This method allows for {@code str} to have an odd length, in which case it will behave
     * exactly as if it had an additional 0 in front.
     *
     * @param str The hexadecimal string to parse, which may or may not start with "0x".
     * @param destinationSize The size of the returned value, which must be big enough to hold the
     *     bytes represented by {@code str}. If it is strictly bigger those bytes from {@code str},
     *     the returned value will be left padded with zeros.
     * @return A value of size {@code destinationSize} corresponding to {@code str} potentially
     *     left-padded.
     * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal
     *     representation, represents more bytes than {@code destinationSize} or {@code
     *     destinationSize < 0}.
     */
    static Bytes fromHexStringLenient(CharSequence str, int destinationSize) {
        checkNotNull(str);
        checkArgument(
                destinationSize >= 0, "Invalid negative destination size %s", destinationSize);
        return BytesValues.fromHexString(str, destinationSize, true);
    }

    /**
     * Parse a hexadecimal string into a {@link Bytes} value.
     *
     * <p>This method requires that {@code str} have an even length.
     *
     * @param str The hexadecimal string to parse, which may or may not start with "0x".
     * @return The value corresponding to {@code str}.
     * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal
     *     representation, or is of an odd length.
     */
    static Bytes fromHexString(CharSequence str) {
        checkNotNull(str);
        return BytesValues.fromHexString(str, -1, false);
    }

    /**
     * Parse a hexadecimal string into a {@link Bytes} value.
     *
     * <p>This method requires that {@code str} have an even length.
     *
     * @param str The hexadecimal string to parse, which may or may not start with "0x".
     * @param destinationSize The size of the returned value, which must be big enough to hold the
     *     bytes represented by {@code str}. If it is strictly bigger those bytes from {@code str},
     *     the returned value will be left padded with zeros.
     * @return A value of size {@code destinationSize} corresponding to {@code str} potentially
     *     left-padded.
     * @throws IllegalArgumentException if {@code str} does correspond to a valid hexadecimal
     *     representation, or is of an odd length.
     * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal
     *     representation, or is of an odd length, or represents more bytes than {@code
     *     destinationSize} or {@code destinationSize < 0}.
     */
    static Bytes fromHexString(CharSequence str, int destinationSize) {
        checkNotNull(str);
        checkArgument(
                destinationSize >= 0, "Invalid negative destination size %s", destinationSize);
        return BytesValues.fromHexString(str, destinationSize, false);
    }

    /**
     * Parse a base 64 string into a {@link Bytes} value.
     *
     * @param str The base 64 string to parse.
     * @return The value corresponding to {@code str}.
     */
    static Bytes fromBase64String(CharSequence str) {
        return Bytes.wrap(Base64.getDecoder().decode(str.toString()));
    }

    /**
     * Generate random bytes.
     *
     * @param size The number of bytes to generate.
     * @return A value containing the desired number of random bytes.
     */
    static Bytes random(int size) {
        return random(size, new SecureRandom());
    }

    /**
     * Generate random bytes.
     *
     * @param size The number of bytes to generate.
     * @param generator The generator for random bytes.
     * @return A value containing the desired number of random bytes.
     */
    static Bytes random(int size, Random generator) {
        byte[] array = new byte[size];
        generator.nextBytes(array);
        return Bytes.wrap(array);
    }

    /**
     * Provides the number of bytes this value represents.
     *
     * @return The number of bytes this value represents.
     */
    int size();

    /**
     * Retrieve a byte in this value.
     *
     * @param i The index of the byte to fetch within the value (0-indexed).
     * @return The byte at index {@code i} in this value.
     * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()}.
     */
    byte get(int i);

    /**
     * Retrieve the 4 bytes starting at the provided index in this value as an integer.
     *
     * @param i The index from which to get the int, which must less than or equal to {@code size()
     *     - 4}.
     * @return An integer whose value is the 4 bytes from this value starting at index {@code i}.
     * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 4}.
     */
    default int getInt(int i) {
        return getInt(i, BIG_ENDIAN);
    }

    /**
     * Retrieve the 4 bytes starting at the provided index in this value as an integer.
     *
     * @param i The index from which to get the int, which must less than or equal to {@code size()
     *     - 4}.
     * @param order The byte-order for decoding the integer.
     * @return An integer whose value is the 4 bytes from this value starting at index {@code i}.
     * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 4}.
     */
    default int getInt(int i, ByteOrder order) {
        int size = size();
        checkElementIndex(i, size);
        if (i > (size - 4)) {
            throw new IndexOutOfBoundsException(
                    format(
                            "Value of size %s has not enough bytes to read a 4 bytes int from index %s",
                            size, i));
        }

        int value = 0;
        if (order == BIG_ENDIAN) {
            value |= ((int) get(i) & 0xFF) << 24;
            value |= ((int) get(i + 1) & 0xFF) << 16;
            value |= ((int) get(i + 2) & 0xFF) << 8;
            value |= ((int) get(i + 3) & 0xFF);
        } else {
            value |= ((int) get(i + 3) & 0xFF) << 24;
            value |= ((int) get(i + 2) & 0xFF) << 16;
            value |= ((int) get(i + 1) & 0xFF) << 8;
            value |= ((int) get(i) & 0xFF);
        }
        return value;
    }

    /**
     * The value corresponding to interpreting these bytes as an integer.
     *
     * @return An value corresponding to this value interpreted as an integer.
     * @throws IllegalArgumentException if {@code size() > 4}.
     */
    default int toInt() {
        return toInt(BIG_ENDIAN);
    }

    /**
     * The value corresponding to interpreting these bytes as an integer.
     *
     * @param order The byte-order for decoding the integer.
     * @return An value corresponding to this value interpreted as an integer.
     * @throws IllegalArgumentException if {@code size() > 4}.
     */
    default int toInt(ByteOrder order) {
        int size = size();
        checkArgument(size <= 4, "Value of size %s has more than 4 bytes", size());
        if (size == 0) {
            return 0;
        }
        if (order == BIG_ENDIAN) {
            int i = size;
            int value = ((int) get(--i) & 0xFF);
            if (i == 0) {
                return value;
            }
            value |= ((int) get(--i) & 0xFF) << 8;
            if (i == 0) {
                return value;
            }
            value |= ((int) get(--i) & 0xFF) << 16;
            if (i == 0) {
                return value;
            }
            return value | ((int) get(--i) & 0xFF) << 24;
        } else {
            int i = 0;
            int value = ((int) get(i) & 0xFF);
            if (++i == size) {
                return value;
            }
            value |= ((int) get(i++) & 0xFF) << 8;
            if (i == size) {
                return value;
            }
            value |= ((int) get(i++) & 0xFF) << 16;
            if (i == size) {
                return value;
            }
            return value | ((int) get(i) & 0xFF) << 24;
        }
    }

    /**
     * Whether this value contains no bytes.
     *
     * @return true if the value contains no bytes
     */
    default boolean isEmpty() {
        return size() == 0;
    }

    /**
     * Retrieves the 8 bytes starting at the provided index in this value as a long.
     *
     * @param i The index from which to get the long, which must less than or equal to {@code size()
     *     - 8}.
     * @return A long whose value is the 8 bytes from this value starting at index {@code i}.
     * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 8}.
     */
    default long getLong(int i) {
        return getLong(i, BIG_ENDIAN);
    }

    /**
     * Retrieves the 8 bytes starting at the provided index in this value as a long.
     *
     * @param i The index from which to get the long, which must less than or equal to {@code size()
     *     - 8}.
     * @param order The byte-order for decoding the integer.
     * @return A long whose value is the 8 bytes from this value starting at index {@code i}.
     * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 8}.
     */
    default long getLong(int i, ByteOrder order) {
        int size = size();
        checkElementIndex(i, size);
        if (i > (size - 8)) {
            throw new IndexOutOfBoundsException(
                    format(
                            "Value of size %s has not enough bytes to read a 8 bytes long from index %s",
                            size, i));
        }

        long value = 0;
        if (order == BIG_ENDIAN) {
            value |= ((long) get(i) & 0xFF) << 56;
            value |= ((long) get(i + 1) & 0xFF) << 48;
            value |= ((long) get(i + 2) & 0xFF) << 40;
            value |= ((long) get(i + 3) & 0xFF) << 32;
            value |= ((long) get(i + 4) & 0xFF) << 24;
            value |= ((long) get(i + 5) & 0xFF) << 16;
            value |= ((long) get(i + 6) & 0xFF) << 8;
            value |= ((long) get(i + 7) & 0xFF);
        } else {
            value |= ((long) get(i + 7) & 0xFF) << 56;
            value |= ((long) get(i + 6) & 0xFF) << 48;
            value |= ((long) get(i + 5) & 0xFF) << 40;
            value |= ((long) get(i + 4) & 0xFF) << 32;
            value |= ((long) get(i + 3) & 0xFF) << 24;
            value |= ((long) get(i + 2) & 0xFF) << 16;
            value |= ((long) get(i + 1) & 0xFF) << 8;
            value |= ((long) get(i) & 0xFF);
        }
        return value;
    }

    /**
     * The value corresponding to interpreting these bytes as a long.
     *
     * @return An value corresponding to this value interpreted as a long.
     * @throws IllegalArgumentException if {@code size() > 8}.
     */
    default long toLong() {
        return toLong(BIG_ENDIAN);
    }

    /**
     * The value corresponding to interpreting these bytes as a long.
     *
     * @param order The byte-order for decoding the integer.
     * @return An value corresponding to this value interpreted as a long.
     * @throws IllegalArgumentException if {@code size() > 8}.
     */
    default long toLong(ByteOrder order) {
        int size = size();
        checkArgument(size <= 8, "Value of size %s has more than 8 bytes", size());
        if (size == 0) {
            return 0;
        }
        if (order == BIG_ENDIAN) {
            int i = size;
            long value = ((long) get(--i) & 0xFF);
            if (i == 0) {
                return value;
            }
            value |= ((long) get(--i) & 0xFF) << 8;
            if (i == 0) {
                return value;
            }
            value |= ((long) get(--i) & 0xFF) << 16;
            if (i == 0) {
                return value;
            }
            value |= ((long) get(--i) & 0xFF) << 24;
            if (i == 0) {
                return value;
            }
            value |= ((long) get(--i) & 0xFF) << 32;
            if (i == 0) {
                return value;
            }
            value |= ((long) get(--i) & 0xFF) << 40;
            if (i == 0) {
                return value;
            }
            value |= ((long) get(--i) & 0xFF) << 48;
            if (i == 0) {
                return value;
            }
            return value | ((long) get(--i) & 0xFF) << 56;
        } else {
            int i = 0;
            long value = ((long) get(i) & 0xFF);
            if (++i == size) {
                return value;
            }
            value |= ((long) get(i) & 0xFF) << 8;
            if (++i == size) {
                return value;
            }
            value |= ((long) get(i) & 0xFF) << 16;
            if (++i == size) {
                return value;
            }
            value |= ((long) get(i) & 0xFF) << 24;
            if (++i == size) {
                return value;
            }
            value |= ((long) get(i) & 0xFF) << 32;
            if (++i == size) {
                return value;
            }
            value |= ((long) get(i) & 0xFF) << 40;
            if (++i == size) {
                return value;
            }
            value |= ((long) get(i) & 0xFF) << 48;
            if (++i == size) {
                return value;
            }
            return value | ((long) get(i) & 0xFF) << 56;
        }
    }

    /**
     * The BigInteger corresponding to interpreting these bytes as a two's-complement signed
     * integer.
     *
     * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement
     *     signed integer.
     */
    default BigInteger toBigInteger() {
        return toBigInteger(BIG_ENDIAN);
    }

    /**
     * The BigInteger corresponding to interpreting these bytes as a two's-complement signed
     * integer.
     *
     * @param order The byte-order for decoding the integer.
     * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement
     *     signed integer.
     */
    default BigInteger toBigInteger(ByteOrder order) {
        if (size() == 0) {
            return BigInteger.ZERO;
        }
        return new BigInteger((order == BIG_ENDIAN) ? toArrayUnsafe() : reverse().toArrayUnsafe());
    }

    /**
     * The BigInteger corresponding to interpreting these bytes as an unsigned integer.
     *
     * @return A positive (or zero) {@link BigInteger} corresponding to interpreting these bytes as
     *     an unsigned integer.
     */
    default BigInteger toUnsignedBigInteger() {
        return toUnsignedBigInteger(BIG_ENDIAN);
    }

    /**
     * The BigInteger corresponding to interpreting these bytes as an unsigned integer.
     *
     * @param order The byte-order for decoding the integer.
     * @return A positive (or zero) {@link BigInteger} corresponding to interpreting these bytes as
     *     an unsigned integer.
     */
    default BigInteger toUnsignedBigInteger(ByteOrder order) {
        return new BigInteger(
                1, (order == BIG_ENDIAN) ? toArrayUnsafe() : reverse().toArrayUnsafe());
    }

    /**
     * Whether this value has only zero bytes.
     *
     * @return {@code true} if all the bits of this value are zeros.
     */
    default boolean isZero() {
        for (int i = size() - 1; i >= 0; --i) {
            if (get(i) != 0) return false;
        }
        return true;
    }

    /**
     * Whether the bytes start with a zero bit value.
     *
     * @return true if the first bit equals zero
     */
    default boolean hasLeadingZero() {
        return size() > 0 && (get(0) & 0x80) == 0;
    }

    /**
     * Provides the number of zero bits preceding the highest-order ("leftmost") one-bit, or {@code
     * size() * 8} if all bits * are zero.
     *
     * @return The number of zero bits preceding the highest-order ("leftmost") one-bit, or {@code
     *     size() * 8} if all bits are zero.
     */
    default int numberOfLeadingZeros() {
        int size = size();
        for (int i = 0; i < size; i++) {
            byte b = get(i);
            if (b == 0) {
                continue;
            }

            return (i * 8) + Integer.numberOfLeadingZeros(b & 0xFF) - 3 * 8;
        }
        return size * 8;
    }

    /**
     * Whether the bytes start with a zero byte value.
     *
     * @return true if the first byte equals zero
     */
    default boolean hasLeadingZeroByte() {
        return size() > 0 && get(0) == 0;
    }

    /**
     * Provides the number of leading zero bytes of the value
     *
     * @return The number of leading zero bytes of the value.
     */
    default int numberOfLeadingZeroBytes() {
        int size = size();
        for (int i = 0; i < size; i++) {
            if (get(i) != 0) {
                return i;
            }
        }
        return size;
    }

    /**
     * Provides the number of trailing zero bytes of the value.
     *
     * @return The number of trailing zero bytes of the value.
     */
    default int numberOfTrailingZeroBytes() {
        int size = size();
        for (int i = size; i >= 1; i--) {
            if (get(i - 1) != 0) {
                return size - i;
            }
        }
        return size;
    }

    /**
     * Provides the number of bits following and including the highest-order ("leftmost") one-bit,
     * or zero if all bits are zero.
     *
     * @return The number of bits following and including the highest-order ("leftmost") one-bit, or
     *     zero if all bits are zero.
     */
    default int bitLength() {
        int size = size();
        for (int i = 0; i < size; i++) {
            byte b = get(i);
            if (b == 0) continue;

            return (size * 8) - (i * 8) - (Integer.numberOfLeadingZeros(b & 0xFF) - 3 * 8);
        }
        return 0;
    }

    /**
     * Return a bit-wise AND of these bytes and the supplied bytes.
     *
     * <p>If this value and the supplied value are different lengths, then the shorter will be
     * zero-padded to the left.
     *
     * @param other The bytes to perform the operation with.
     * @return The result of a bit-wise AND.
     */
    default Bytes and(Bytes other) {
        return and(other, MutableBytes.create(Math.max(size(), other.size())));
    }

    /**
     * Calculate a bit-wise AND of these bytes and the supplied bytes.
     *
     * <p>If this value or the supplied value are shorter in length than the output vector, then
     * they will be zero-padded to the left. Likewise, if either this value or the supplied valid is
     * longer in length than the output vector, then they will be truncated to the left.
     *
     * @param other The bytes to perform the operation with.
     * @param result The mutable output vector for the result.
     * @param <T> The {@link MutableBytes} value type.
     * @return The {@code result} output vector.
     */
    default <T extends MutableBytes> T and(Bytes other, T result) {
        checkNotNull(other);
        checkNotNull(result);
        int rSize = result.size();
        int offsetSelf = rSize - size();
        int offsetOther = rSize - other.size();
        for (int i = 0; i < rSize; i++) {
            byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
            byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
            result.set(i, (byte) (b1 & b2));
        }
        return result;
    }

    /**
     * Return a bit-wise OR of these bytes and the supplied bytes.
     *
     * <p>If this value and the supplied value are different lengths, then the shorter will be
     * zero-padded to the left.
     *
     * @param other The bytes to perform the operation with.
     * @return The result of a bit-wise OR.
     */
    default Bytes or(Bytes other) {
        return or(other, MutableBytes.create(Math.max(size(), other.size())));
    }

    /**
     * Calculate a bit-wise OR of these bytes and the supplied bytes.
     *
     * <p>If this value or the supplied value are shorter in length than the output vector, then
     * they will be zero-padded to the left. Likewise, if either this value or the supplied valid is
     * longer in length than the output vector, then they will be truncated to the left.
     *
     * @param other The bytes to perform the operation with.
     * @param result The mutable output vector for the result.
     * @param <T> The {@link MutableBytes} value type.
     * @return The {@code result} output vector.
     */
    default <T extends MutableBytes> T or(Bytes other, T result) {
        checkNotNull(other);
        checkNotNull(result);
        int rSize = result.size();
        int offsetSelf = rSize - size();
        int offsetOther = rSize - other.size();
        for (int i = 0; i < rSize; i++) {
            byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
            byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
            result.set(i, (byte) (b1 | b2));
        }
        return result;
    }

    /**
     * Return a bit-wise XOR of these bytes and the supplied bytes.
     *
     * <p>If this value and the supplied value are different lengths, then the shorter will be
     * zero-padded to the left.
     *
     * @param other The bytes to perform the operation with.
     * @return The result of a bit-wise XOR.
     */
    default Bytes xor(Bytes other) {
        return xor(other, MutableBytes.create(Math.max(size(), other.size())));
    }

    /**
     * Calculate a bit-wise XOR of these bytes and the supplied bytes.
     *
     * <p>If this value or the supplied value are shorter in length than the output vector, then
     * they will be zero-padded to the left. Likewise, if either this value or the supplied valid is
     * longer in length than the output vector, then they will be truncated to the left.
     *
     * @param other The bytes to perform the operation with.
     * @param result The mutable output vector for the result.
     * @param <T> The {@link MutableBytes} value type.
     * @return The {@code result} output vector.
     */
    default <T extends MutableBytes> T xor(Bytes other, T result) {
        checkNotNull(other);
        checkNotNull(result);
        int rSize = result.size();
        int offsetSelf = rSize - size();
        int offsetOther = rSize - other.size();
        for (int i = 0; i < rSize; i++) {
            byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
            byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
            result.set(i, (byte) (b1 ^ b2));
        }
        return result;
    }

    /**
     * Return a bit-wise NOT of these bytes.
     *
     * @return The result of a bit-wise NOT.
     */
    default Bytes not() {
        return not(MutableBytes.create(size()));
    }

    /**
     * Calculate a bit-wise NOT of these bytes.
     *
     * <p>If this value is shorter in length than the output vector, then it will be zero-padded to
     * the left. Likewise, if this value is longer in length than the output vector, then it will be
     * truncated to the left.
     *
     * @param result The mutable output vector for the result.
     * @param <T> The {@link MutableBytes} value type.
     * @return The {@code result} output vector.
     */
    default <T extends MutableBytes> T not(T result) {
        checkNotNull(result);
        int rSize = result.size();
        int offsetSelf = rSize - size();
        for (int i = 0; i < rSize; i++) {
            byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
            result.set(i, (byte) ~b1);
        }
        return result;
    }

    /**
     * Shift all bits in this value to the right.
     *
     * @param distance The number of bits to shift by.
     * @return A value containing the shifted bits.
     */
    default Bytes shiftRight(int distance) {
        return shiftRight(distance, MutableBytes.create(size()));
    }

    /**
     * Shift all bits in this value to the right.
     *
     * <p>If this value is shorter in length than the output vector, then it will be zero-padded to
     * the left. Likewise, if this value is longer in length than the output vector, then it will be
     * truncated to the left (after shifting).
     *
     * @param distance The number of bits to shift by.
     * @param result The mutable output vector for the result.
     * @param <T> The {@link MutableBytes} value type.
     * @return The {@code result} output vector.
     */
    default <T extends MutableBytes> T shiftRight(int distance, T result) {
        checkNotNull(result);
        int rSize = result.size();
        int offsetSelf = rSize - size();

        int d = distance / 8;
        int s = distance % 8;
        int resIdx = rSize - 1;
        for (int i = rSize - 1 - d; i >= 0; i--) {
            byte res;
            if (i < offsetSelf) {
                res = 0;
            } else {
                int selfIdx = i - offsetSelf;
                int leftSide = (get(selfIdx) & 0xFF) >>> s;
                int rightSide = (selfIdx == 0) ? 0 : get(selfIdx - 1) << (8 - s);
                res = (byte) (leftSide | rightSide);
            }
            result.set(resIdx--, res);
        }
        for (; resIdx >= 0; resIdx--) {
            result.set(resIdx, (byte) 0);
        }
        return result;
    }

    /**
     * Shift all bits in this value to the left.
     *
     * @param distance The number of bits to shift by.
     * @return A value containing the shifted bits.
     */
    default Bytes shiftLeft(int distance) {
        return shiftLeft(distance, MutableBytes.create(size()));
    }

    /**
     * Shift all bits in this value to the left.
     *
     * <p>If this value is shorter in length than the output vector, then it will be zero-padded to
     * the left. Likewise, if this value is longer in length than the output vector, then it will be
     * truncated to the left.
     *
     * @param distance The number of bits to shift by.
     * @param result The mutable output vector for the result.
     * @param <T> The {@link MutableBytes} value type.
     * @return The {@code result} output vector.
     */
    default <T extends MutableBytes> T shiftLeft(int distance, T result) {
        checkNotNull(result);
        int size = size();
        int rSize = result.size();
        int offsetSelf = rSize - size;

        int d = distance / 8;
        int s = distance % 8;
        int resIdx = 0;
        for (int i = d; i < rSize; i++) {
            byte res;
            if (i < offsetSelf) {
                res = 0;
            } else {
                int selfIdx = i - offsetSelf;
                int leftSide = get(selfIdx) << s;
                int rightSide = (selfIdx == size - 1) ? 0 : (get(selfIdx + 1) & 0xFF) >>> (8 - s);
                res = (byte) (leftSide | rightSide);
            }
            result.set(resIdx++, res);
        }
        for (; resIdx < rSize; resIdx++) {
            result.set(resIdx, (byte) 0);
        }
        return result;
    }

    /**
     * Create a new value representing (a view of) a slice of the bytes of this value.
     *
     * <p>Please note that the resulting slice is only a view and as such maintains a link to the
     * underlying full value. So holding a reference to the returned slice may hold more memory than
     * the slide represents. Use {@link #copy} on the returned slice if that is not what you want.
     *
     * @param i The start index for the slice.
     * @return A new value providing a view over the bytes from index {@code i} (included) to the
     *     end.
     * @throws IndexOutOfBoundsException if {@code i < 0}.
     */
    default Bytes slice(int i) {
        if (i == 0) {
            return this;
        }
        int size = size();
        if (i >= size) {
            return EMPTY;
        }
        return slice(i, size - i);
    }

    /**
     * Create a new value representing (a view of) a slice of the bytes of this value.
     *
     * <p>Please note that the resulting slice is only a view and as such maintains a link to the
     * underlying full value. So holding a reference to the returned slice may hold more memory than
     * the slide represents. Use {@link #copy} on the returned slice if that is not what you want.
     *
     * @param i The start index for the slice.
     * @param length The length of the resulting value.
     * @return A new value providing a view over the bytes from index {@code i} (included) to {@code
     *     i + length} (excluded).
     * @throws IllegalArgumentException if {@code length < 0}.
     * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()} or {i + length > size()}
     *     .
     */
    Bytes slice(int i, int length);

    /**
     * Return a value equivalent to this one but guaranteed to 1) be deeply immutable (i.e. the
     * underlying value will be immutable) and 2) to not retain more bytes than exposed by the
     * value.
     *
     * @return A value, equals to this one, but deeply immutable and that doesn't retain any
     *     "unreachable" bytes. For performance reasons, this is allowed to return this value
     *     however if it already fit those constraints.
     */
    Bytes copy();

    /**
     * Return a new mutable value initialized with the content of this value.
     *
     * @return A mutable copy of this value. This will copy bytes, modifying the returned value will
     *     <b>not</b> modify this value.
     */
    MutableBytes mutableCopy();

    /**
     * Copy the bytes of this value to the provided mutable one, which must have the same size.
     *
     * @param destination The mutable value to which to copy the bytes to, which must have the same
     *     size as this value. If you want to copy value where size differs, you should use {@link
     *     #slice} and/or {@link MutableBytes#mutableSlice} and apply the copy to the result.
     * @throws IllegalArgumentException if {@code this.size() != destination.size()}.
     */
    default void copyTo(MutableBytes destination) {
        checkNotNull(destination);
        checkArgument(
                destination.size() == size(),
                "Cannot copy %s bytes to destination of non-equal size %s",
                size(),
                destination.size());
        copyTo(destination, 0);
    }

    /**
     * Copy the bytes of this value to the provided mutable one from a particular offset.
     *
     * <p>This is a (potentially slightly more efficient) shortcut for {@code
     * copyTo(destination.mutableSlice(destinationOffset, this.size()))}.
     *
     * @param destination The mutable value to which to copy the bytes to, which must have enough
     *     bytes from {@code destinationOffset} for the copied value.
     * @param destinationOffset The offset in {@code destination} at which the copy starts.
     * @throws IllegalArgumentException if the destination doesn't have enough room, that is if
     *     {@code this.size() > (destination.size() - destinationOffset)}.
     */
    default void copyTo(MutableBytes destination, int destinationOffset) {
        checkNotNull(destination);

        // Special casing an empty source or the following checks might throw (even though we have
        // nothing to copy anyway) and this gets inconvenient for generic methods using copyTo() as
        // they may have to special case empty values because of this. As an example,
        // concatenate(EMPTY, EMPTY) would need to be special cased without this.
        int size = size();
        if (size == 0) {
            return;
        }

        checkElementIndex(destinationOffset, destination.size());
        checkArgument(
                destination.size() - destinationOffset >= size,
                "Cannot copy %s bytes, destination has only %s bytes from index %s",
                size,
                destination.size() - destinationOffset,
                destinationOffset);

        for (int i = 0; i < size; i++) {
            destination.set(destinationOffset + i, get(i));
        }
    }

    /**
     * Append the bytes of this value to the {@link ByteBuffer}.
     *
     * @param byteBuffer The {@link ByteBuffer} to which to append this value.
     * @throws BufferOverflowException If the writer attempts to write more than the provided buffer
     *     can hold.
     * @throws ReadOnlyBufferException If the provided buffer is read-only.
     */
    default void appendTo(ByteBuffer byteBuffer) {
        checkNotNull(byteBuffer);
        for (int i = 0; i < size(); i++) {
            byteBuffer.put(get(i));
        }
    }

    /**
     * Append the bytes of this value to the provided Vert.x {@link Buffer}.
     *
     * <p>Note that since a Vert.x {@link Buffer} will grow as necessary, this method never fails.
     *
     * @param buffer The {@link Buffer} to which to append this value.
     */
    default void appendTo(Buffer buffer) {
        checkNotNull(buffer);
        for (int i = 0; i < size(); i++) {
            buffer.appendByte(get(i));
        }
    }

    /**
     * Append this value as a sequence of hexadecimal characters.
     *
     * @param appendable The appendable
     * @param <T> The appendable type.
     * @return The appendable.
     */
    default <T extends Appendable> T appendHexTo(T appendable) {
        try {
            int size = size();
            for (int i = 0; i < size; i++) {
                byte b = get(i);
                appendable.append(AbstractBytes.HEX_CODE[b >> 4 & 15]);
                appendable.append(AbstractBytes.HEX_CODE[b & 15]);
            }
            return appendable;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Return the number of bytes in common between this set of bytes and another.
     *
     * @param other The bytes to compare to.
     * @return The number of common bytes.
     */
    default int commonPrefixLength(Bytes other) {
        checkNotNull(other);
        int ourSize = size();
        int otherSize = other.size();
        int i = 0;
        while (i < ourSize && i < otherSize && get(i) == other.get(i)) {
            i++;
        }
        return i;
    }

    /**
     * Return a slice over the common prefix between this set of bytes and another.
     *
     * @param other The bytes to compare to.
     * @return A slice covering the common prefix.
     */
    default Bytes commonPrefix(Bytes other) {
        return slice(0, commonPrefixLength(other));
    }

    /**
     * Return a slice of representing the same value but without any leading zero bytes.
     *
     * @return {@code value} if its left-most byte is non zero, or a slice that exclude any leading
     *     zero bytes.
     */
    default Bytes trimLeadingZeros() {
        int size = size();
        for (int i = 0; i < size; i++) {
            if (get(i) != 0) {
                return slice(i);
            }
        }
        return Bytes.EMPTY;
    }

    /**
     * Update the provided message digest with the bytes of this value.
     *
     * @param digest The digest to update.
     */
    default void update(MessageDigest digest) {
        checkNotNull(digest);
        digest.update(toArrayUnsafe());
    }

    /**
     * Computes the reverse array of bytes of the current bytes.
     *
     * @return a new Bytes value, containing the bytes in reverse order
     */
    default Bytes reverse() {
        byte[] reverse = new byte[size()];
        for (int i = 0; i < size(); i++) {
            reverse[size() - i - 1] = get(i);
        }
        return Bytes.wrap(reverse);
    }

    /**
     * Extract the bytes of this value into a byte array.
     *
     * @return A byte array with the same content than this value.
     */
    default byte[] toArray() {
        return toArray(BIG_ENDIAN);
    }

    /**
     * Extract the bytes of this value into a byte array.
     *
     * @param byteOrder the byte order to apply : big endian or little endian
     * @return A byte array with the same content than this value.
     */
    default byte[] toArray(ByteOrder byteOrder) {
        int size = size();
        byte[] array = new byte[size];
        if (byteOrder == BIG_ENDIAN) {
            for (int i = 0; i < size; i++) {
                array[i] = get(i);
            }
        } else {
            for (int i = 0; i < size(); i++) {
                array[size() - i - 1] = get(i);
            }
        }
        return array;
    }

    /**
     * Get the bytes represented by this value as byte array.
     *
     * <p>Contrarily to {@link #toArray()}, this may avoid allocating a new array and directly
     * return the backing array of this value if said value is array backed and doing so is
     * possible. As such, modifications to the returned array may or may not impact this value. As
     * such, this method should be used with care and hence the "unsafe" moniker.
     *
     * @return A byte array with the same content than this value, which may or may not be the
     *     direct backing of this value.
     */
    default byte[] toArrayUnsafe() {
        return toArray();
    }

    /**
     * Return the hexadecimal string representation of this value.
     *
     * @return The hexadecimal representation of this value, starting with "0x".
     */
    @Override
    String toString();

    /**
     * Provides this value represented as hexadecimal, starting with "0x".
     *
     * @return This value represented as hexadecimal, starting with "0x".
     */
    default String toHexString() {
        return appendHexTo(new StringBuilder("0x")).toString();
    }

    /**
     * Provides this value represented as hexadecimal, with no prefix
     *
     * @return This value represented as hexadecimal, with no prefix.
     */
    default String toUnprefixedHexString() {
        return appendHexTo(new StringBuilder()).toString();
    }

    default String toEllipsisHexString() {
        int size = size();
        if (size < 6) {
            return toHexString();
        }
        StringBuilder appendable = new StringBuilder("0x");
        for (int i = 0; i < 2; i++) {
            byte b = get(i);
            appendable.append(AbstractBytes.HEX_CODE[b >> 4 & 15]);
            appendable.append(AbstractBytes.HEX_CODE[b & 15]);
        }
        appendable.append("..");
        for (int i = 0; i < 2; i++) {
            byte b = get(i + size - 2);
            appendable.append(AbstractBytes.HEX_CODE[b >> 4 & 15]);
            appendable.append(AbstractBytes.HEX_CODE[b & 15]);
        }
        return appendable.toString();
    }

    /**
     * Provides this value represented as a minimal hexadecimal string (without any leading zero)
     *
     * @return This value represented as a minimal hexadecimal string (without any leading zero).
     */
    default String toShortHexString() {
        StringBuilder hex = appendHexTo(new StringBuilder());

        int i = 0;
        while (i < hex.length() && hex.charAt(i) == '0') {
            i++;
        }
        return "0x" + hex.substring(i);
    }

    /**
     * Provides this value represented as a minimal hexadecimal string (without any leading zero,
     * except if it's valued zero or empty, in which case it returns 0x0).
     *
     * @return This value represented as a minimal hexadecimal string (without any leading zero,
     *     except if it's valued zero or empty, in which case it returns 0x0).
     */
    default String toQuantityHexString() {
        if (Bytes.EMPTY.equals(this)) {
            return "0x0";
        }
        StringBuilder hex = appendHexTo(new StringBuilder());

        int i = 0;
        while (i < hex.length() - 1 && hex.charAt(i) == '0') {
            i++;
        }
        return "0x" + hex.substring(hex.charAt(hex.length() - 1) == '0' ? i : i++);
    }

    /**
     * Provides this value represented as base 64
     *
     * @return This value represented as base 64.
     */
    default String toBase64String() {
        return Base64.getEncoder().encodeToString(toArrayUnsafe());
    }

    @Override
    default int compareTo(Bytes b) {
        checkNotNull(b);

        int sizeCmp = Integer.compare(bitLength(), b.bitLength());
        if (sizeCmp != 0) {
            return sizeCmp;
        }

        for (int i = 0; i < size(); i++) {
            int cmp = Integer.compare(get(i) & 0xff, b.get(i) & 0xff);
            if (cmp != 0) {
                return cmp;
            }
        }
        return 0;
    }
}
